local get_relevant_players = limbo_respawning.get_living_players

local vector_mul = vector.multiply
local vector_add = vector.add
local vector_dist = vector.distance

local raycast = minetest.raycast
local get_node = minetest.get_node

local mobs = defense_mob_api
mobs.gravity = -9.81
mobs.default_prototype = {
	-- minetest properties & defaults
	physical = true,
	collide_with_objects = true,
	makes_footstep_sound = true,
	visual = "mesh",
	automatic_face_movement_dir = true,
	stepheight = 0.6,
	-- custom properties
	id = 0, -- Automatically set
	smart_path = true, -- Use the pathfinder
	--organize in a swarm
	swarm = false,

	--for switching pathfinding on and off depending on line of line_of_sight
	--if negative (default) it's always on, if >= 0 it'll switch
	smart_path_for = -1,
	mass = 1,
	movement = "ground", -- "ground"/"air"
	move_speed = 1,
	jump_height = 1,
	armor = 0,
	attack_range = 1,
	attack_damage = 1,
	attack_interval = 1,


	current_animation = nil,
	current_animation_end = 0,
	destination = nil, -- position
	last_attack_time = 0,
	life_timer = 75,
	pause_timer = 0,
	timer = 0,
	media_prefix = "modname_mobname_", --set properly in register_mob
	idle_sound_cd = 5,
	time_since_last_idle_sound = 0,
	view_range = 20,



	-- cache
	cache_is_standing = nil,
	cache_find_nearest_player = nil,
}

local reg_nodes = minetest.registered_nodes

local function vec_zero() return {x=0, y=0, z=0} end




do --look
	function mobs.default_prototype:look(dir)
		local pos = self.object:get_pos()
		local dest = vector_add(pos, vector_mul(dir, self.view_range))

		local ray = raycast(pos, dest, true, true)

		local b = true
		while b
		do
			b = false
			local pt = ray:next()
			if pt
			then
				if pt.type == node
				then
					local node = get_node(pt.under)
					if node and node.name ~= "ignore"
					then
						if not(reg_nodes[node.name] or {walkable = true}).walkable
						then
							b = true
						end
					end
					if not b
					then
						return false, pt.under
					end
				end
				--[[elseif pt.type == object and
					vector_dist(pt.intersection_point, pos) > 2
				then
					return false
				end]]
			end
		end
		return true
	end
end






































function mobs.default_prototype:make_sound(event_name, detached, gain, pitch, max_hear_dist)
	local def =
	{
		object = self.object,
		gain = gain or 0.5 + math.random() * 0.5,
		pitch = pitch or 1 + (math.random() - 0.5) * 0.5,
		max_hear_distance = max_hear_dist,
	}
	if detached then def.object = nil end

	return minetest.sound_play(self.media_prefix .. event_name, def)
end

--on_activate

do--by using local scopes we can re-use the names for observer storage tables


	local observers, ocount = {}, 0
	--for registering functions as callback
	mobs.register_on_default_activate = function (func)
		ocount = ocount + 1
		observers[ocount] = func
	end

	function mobs.default_prototype:on_activate(staticdata)
		for i = 1, ocount
		do
			observers[i](self, staticdata)
		end

		self.object:set_armor_groups({fleshy = 100 - self.armor})
		if self.movement ~= "air" then
			self.object:setacceleration({x=0, y=mobs.gravity, z=0})
		end
		self.time_since_last_idle_sound = math.random() * self.idle_sound_cd
		self.id = math.random(0, 100000)
	end

end

do --safe remove
	local observers, ocount = {}, 0
	--for registering functions as callback
	mobs.register_on_default_remove = function(func)
		ocount = ocount + 1
		observers[ocount] = func
	end


	function mobs.default_prototype:remove()
		for i = 1, ocount
		do
			observers[i](self)
		end

		self.removed = true
		self.object:remove()
	end
end


--on_step
do
	local observers, ocount = {}, 0
	--for registering functions as callback
	mobs.register_on_default_step = function(func)
		ocount = ocount + 1
		observers[ocount] = func
	end


	function mobs.default_prototype:on_step(dtime)
		local ceas = os.clock()

		for i = 1, ocount
		do
			observers[i](self, dtime)
		end


		self.cache_is_standing = nil
		self.cache_find_nearest_player = nil


		self.time_since_last_idle_sound = self.time_since_last_idle_sound + dtime
		if self.idle_sound_cd < self.time_since_last_idle_sound and
			math.random() < 0.3
		then
			self:make_sound("idle")
			self.time_since_last_idle_sound = 0
		end


		if self.pause_timer <= 0 then
			if self.destination then
				self:move(dtime, self.destination)
				if vector.distance(self.object:get_pos(), self.destination) < 0.5 then
					self.destination = nil
				end
			else
				self:move(dtime, self.object:get_pos())
			end
		else
			self.pause_timer = self.pause_timer - dtime
		end

		if self.movement ~= "air" and not self:is_standing() then
			self:set_animation("fall", {"jump", "attack", "move_attack"})
		end



		-- Remove when far enough and may not reach the player at all
		local nearest = self:find_target()
		if self.life_timer <= 0 then
			if nearest.distance > 12 then
				self:remove()
			end
		else
			self.life_timer = self.life_timer - dtime
		end

		-- Disable collision when far enough
		if self.collide_with_objects then
			if nearest.distance > 6 then
				self.collide_with_objects = false
				self.object:set_properties({collide_with_objects = self.collide_with_objects})
			end
		else
			if nearest.distance < 1.5 then
				self.collide_with_objects = true
				self.object:set_properties({collide_with_objects = self.collide_with_objects})
			end
		end

		self.timer = self.timer + dtime
		defense_mob_api:track_time("prototype on_step", os.clock() - ceas)
	end

end


--on_punch
do
	local observers, ocount = {}, 0
	--for registering functions as callback
	mobs.register_on_default_punch = function(func)
		ocount = ocount + 1
		observers[ocount] = func
	end



	function mobs.default_prototype:on_punch(puncher, time_from_last_punch, tool_capabilities, dir, damage)

		for i = 1, ocount
		do
			observers[i](self, puncher, time_from_last_punch, tool_capabilities, dir, damage)
		end

		-- Weapon wear code adapted from TenPlus1's mobs redo (https://github.com/tenplus1/mobs)
		if puncher
		then
			local weapon = puncher:get_wielded_item()
			if tool_capabilities
			then
				local wear = (0.01) * (self.armor / 100) * 65534 + 1
				weapon:add_wear(wear)
				puncher:set_wielded_item(weapon)
			end
		end
		self:make_sound("hurt")

		dir.y = dir.y + 1
		local m = self.mass or 1
		local knockback = vector.multiply(vector.normalize(dir), 10 / (1 + m))
		self.object:setvelocity(vector.add(self.object:getvelocity(), knockback))
		self.pause_timer = 0.3

		if self.object:get_hp() - damage <= 0
		then
			self:die(puncher)
		end
	end

end

do --get_staticdata
	local observers, ocount = {}, 0
	--for registering functions as callback
	mobs.register_on_default_get_staticdata = function(func)
		ocount = ocount + 1
		observers[ocount] = func
	end

	function mobs.default_prototype:get_staticdata()
		local data = {}
		for i = 1, ocount
		do
			local key, val = observers[i](self)
			data[key] = val
		end

		return minetest.serialize(data)
	end
end

function mobs.default_prototype:damage(amount)
	local hp = self.object:get_hp()
	if hp <= amount then
		self:die()
	else
		self.object:set_hp(hp - amount)
	end
end


do --attack

	local observers, ocount = {}, 0
	--for registering functions as callback
	mobs.register_on_default_attack = function(func)
		ocount = ocount + 1
		observers[ocount] = func
	end

	function mobs.default_prototype:attack(obj, dir)
		for i = 1, ocount
		do
			observers[i](self, obj, dir)
		end

		self:make_sound("attack")
		obj:punch(self.object, self.timer - self.last_attack_time,  {
			full_punch_interval=self.attack_interval,
			damage_groups = {fleshy=self.attack_damage}
		}, dir)
	end

end

function mobs.default_prototype:move(dtime, destination)
	mobs.move_method[self.movement](self, dtime, destination)
end

-- Attack the player
function mobs.default_prototype:hunt()
	local nearest = self:find_target()

	if nearest.player then
		if nearest.distance <= self.attack_range then
			self:do_attack(nearest.player)
		end

		if nearest.distance > self.attack_range or nearest.distance < self.attack_range/2-1 then
			local pos = self.object:get_pos()

			local smart_dest = nil
			if self.smart_path then --Do pathfinding
				if self.smart_path_for > 0
				then
					self.smart_path_for = self.smart_path_for - 1
				end

				--add a little smarts if needed
				if self.smart_path_for >= 0 --this way it stays on if it's negative
				then
					local eyepos = {x = pos.x, y = pos.y + 0.5, z = pos.z}
					local los, obstacle = minetest.line_of_sight(nearest.position, eyepos)
					if obstacle and vector.distance(eyepos, obstacle) < 10
					then
						self.smart_path_for = self.smart_path_for + 2
					end
				end
				if smart_path_for ~= 0
				then
					smart_dest = defense_mob_api.pathfinder:get_waypoint(self.name, pos.x, pos.y, pos.z)
				end
			end

			if smart_dest then
				self.destination = smart_dest
			else
				local r = math.max(0, self.attack_range - 2)
				local dir = vector.aim(nearest.position, pos)
				self.destination = vector.add(nearest.position, vector.multiply(dir, r))
			end

		end
	end
end

function mobs.default_prototype:do_attack(obj)
	if self.last_attack_time + self.attack_interval < self.timer then
		local dir = vector.aim(self.object:get_pos(), obj:get_pos())
		self:attack(obj, dir)
		self.last_attack_time = self.timer

		if self.current_animation == "move" then
			self:set_animation("move_attack")
		else
			self:set_animation("attack")
		end
	end

	self.life_timer = math.min(300, self.life_timer + 60)
end

function mobs.default_prototype:jump(direction)
	if self:is_standing() then
		if direction then
			direction.y = 0.1
			direction = vector.normalize(direction)
		else
			direction = vec_zero()
		end

		local v = self.object:getvelocity()
		v.y = math.sqrt(2 * -mobs.gravity * (self.jump_height + 0.2))
		v.x = direction.x * self.jump_height
		v.z = direction.z * self.jump_height
		self.object:setvelocity(vector.add(self.object:getvelocity(), v))

		self:set_animation("jump")
	end
end

do --die

	local observers, ocount = {}, 0
	--for registering functions as callback
	mobs.register_on_default_die = function(func)
		ocount = ocount + 1
		observers[ocount] = func
	end

	function mobs.default_prototype:die(killer)
		for i = 1, ocount
		do
			observers[i](self, killer)
		end
		self.dead = true
		self:make_sound("die", true)
		self:on_death(killer)
		self:remove()
	end

end



function mobs.default_prototype:on_death(killer)

end

function mobs.default_prototype:is_standing()
	if self.cache_is_standing ~= nil then
		return self.cache_is_standing
	end

	if self.movement == "air" then
		self.cache_is_standing = false
		return false
	end

	if self.object:getvelocity().y ~= 0 then
		self.cache_is_standing = false
		return false
	end

	-- Check the four bottom corners for collision
	local p = self.object:get_pos()
	p.y = p.y + self.collisionbox[2] - 0.25
	local corners = {
		vector.add(p, {x=self.collisionbox[1], y=0, z=self.collisionbox[3]}),
		vector.add(p, {x=self.collisionbox[1], y=0, z=self.collisionbox[6]}),
		vector.add(p, {x=self.collisionbox[4], y=0, z=self.collisionbox[3]}),
		vector.add(p, {x=self.collisionbox[4], y=0, z=self.collisionbox[6]}),
	}
	for _,c in ipairs(corners) do
		local node = minetest.get_node_or_nil(c)

		if not node or
			(reg_nodes[node.name] or {walkable = true}).walkable
		then
			self.cache_is_standing = true
			return true
		end
	end

	self.cache_is_standing = false
	return false
end

function mobs.default_prototype:set_animation(name, inhibit)
	if self.current_animation == name then
		return
	end
	if inhibit then
		for _,p in ipairs(inhibit) do
			if self.current_animation == p and self.timer < self.current_animation_end then
				return
			end
		end
	end

	local anim_prop = self.animation[name]
	if anim_prop then
		self.current_animation = name
		self.current_animation_end = self.timer + (anim_prop.b - anim_prop.a - 1) / anim_prop.rate
		self.object:set_animation({x=anim_prop.a, y=anim_prop.b}, anim_prop.rate, 0)
	end
end

function mobs.default_prototype:find_target()
	if self.cache_find_nearest_player ~= nil then
		return self.cache_find_nearest_player
	end

	local p = self.object:get_pos()
	local nearest_player = nil
	local nearest_pos = p
	local nearest_dist = math.huge
	for _,obj in ipairs(get_relevant_players()) do --TODO change this
		local pos = obj:get_pos()
		pos.y = pos.y + 1
		local d = vector.distance(pos, p)
		if d < nearest_dist then
			nearest_player = obj
			nearest_pos = pos
			nearest_dist = d
		end
	end

	local ret = {player=nearest_player or self.object, position=nearest_pos, distance=nearest_dist}
	self.cache_find_nearest_player = ret
	return ret
end


-- Movement implementations for the default movement types
mobs.move_method = {}
function mobs.move_method:air(dtime, destination)
	local delta = vector.subtract(destination, self.object:get_pos())
	local dist = vector.length(delta)

	if dist ~= dist --nan check
	then
		return
	end

	-- Add random variation
	if dist > 3 then
		local r_angle = (self.id/100000) * 2 * math.pi
		local r_radius = (self.id/100000) * (dist - 3)/3
		delta = vector.add(delta, {
			x=math.cos(r_angle)*r_radius,
			y=r_radius,
			z=math.sin(r_angle)*r_radius
		})
	end

	-- Compute speed and smoothing factor
	local speed = self.move_speed * math.max(0, math.min(1, 1.2 * dist))
	local t
	local v = self.object:getvelocity()
	if vector.length(v) < self.move_speed * 1.5 then
		t = math.pow(0.1, dtime)
	else
		t = math.pow(0.4, dtime)
		speed = speed * 0.9
	end




	--[[Note:
		in lua
		x = a and b or c
		means
		if a then x = b else x = c
	]]
	-- Compute and set resulting velocity
	self.object:setvelocity(vector.add(
		vector.multiply(v, t),
		vector.multiply(dist > 0 and vector.normalize(delta) or vec_zero(), speed * (1-t))
	))

	if speed > self.move_speed * 0.04 then
		self:set_animation("move", {"attack", "move_attack"})
	else
		self:set_animation("idle", {"attack", "move_attack"})
	end
end
function mobs.move_method:ground(dtime, destination)
	local delta = vector.subtract(destination, self.object:get_pos())
	delta.y = 0
	local dist = vector.length(delta)

	if dist ~= dist --nan check
	then
		return
	end

	-- Add random variation
	if dist > 4 then
		local r_angle = (self.id/100000) * 2 * math.pi
		local r_radius = (dist - 4)/4
		delta = vector.add(delta, {
			x=math.cos(r_angle)*r_radius,
			y=0,
			z=math.sin(r_angle)*r_radius
	})
	end

	-- Compute speed and smoothing factor
	local speed = self.move_speed * math.max(0, math.min(1, 1.2 * dist))
	local t
	local v = self.object:getvelocity()
	if self:is_standing() and vector.length(v) < self.move_speed * 4 then
		t = math.pow(0.001, dtime)
	else
		t = math.pow(0.4, dtime)
		speed = speed * 0.9
	end

	-- Compute and set resulting velocity
	local dir = dist > 0 and vector.normalize(delta) or vec_zero()
	local v2 = vector.add(
		vector.multiply(v, t),
		vector.multiply(dir, speed * (1-t))
	)
	v2.y = v.y
	self.object:setvelocity(v2)


	local jump = nil
	local pos = self.object:get_pos()

	-- Check for jump
	if dist > 1 then
		--Jump over obstacles
		local p = vector.new(pos)
		p.y = p.y + self.collisionbox[2] + 0.5
		local sx = self.collisionbox[4] - self.collisionbox[1]
		local sz = self.collisionbox[6] - self.collisionbox[3]
		local r = math.sqrt(sx*sx + sz*sz)/2 + 0.5
		local fronts = {} --might be worth caching those
		do
			local xedge, zedge
			local offset = 0.2
			if math.sign(delta.x) < 0
			then
				xedge = self.collisionbox[1] - offset
			else
				xedge = self.collisionbox[4] + offset
			end
			if math.sign(delta.z) < 0
			then
				zedge = self.collisionbox[3] - offset
			else
				zedge = self.collisionbox[6] + offset
			end

			local index = 1
			--point to each node on the sides
			for i = 0, sz
			do
				fronts[index] = {x = self.collisionbox[1] + i, y = 0, z = zedge}
				index = index + 1
			end
			for i = 0, sx
			do
				fronts[index] = {x = xedge, y = 0, z = self.collisionbox[3] + i}
				index = index + 1
			end
			--also check the corner
			fronts[index] = {x = xedge, y = 0, z = zedge}
		end


		for _,f in ipairs(fronts) do
			local node = minetest.get_node_or_nil(vector.add(p, f))

			-- or {walkable = true} snippet to avoid indexing nil values with unknown nodes
			if not node or
				(reg_nodes[node.name] or {walkable = true}).walkable
			then
				jump = vector.aim(pos, destination)
				break
			end
		end
	elseif destination.y > pos.y + 1
	then
		jump = vector.aim(pos, destination)
	end


	if jump then
		self:jump(jump)
	elseif self:is_standing() then
		if speed > self.move_speed * 0.06 then
			self:set_animation("move", {"move_attack"})
		else
			self:set_animation("idle", {"attack", "move_attack"})
		end
	end
end



function mobs.register_mob(name, def)
	local prototype = {}
	--copy default prototype
	for k,v in pairs(mobs.default_prototype) do
		prototype[k] = v
	end
	--overwrite fields of default prototype that def uses
	for k,v in pairs(def) do
		prototype[k] = v
	end
	prototype.media_prefix = (string.gsub(name, ":", "_") .. "_")


	prototype.move = def.move or mobs.move_method[prototype.movement]

	-- Register for pathfinding
	if defense_mob_api.pathfinder and prototype.smart_path then
		defense_mob_api.pathfinder:register_class(name, {
			collisionbox = prototype.collisionbox,
			jump_height = math.floor(prototype.jump_height),
			path_check = def.pathfinder_check or defense_mob_api.pathfinder.default_path_check[prototype.movement],
			cost_method = def.pathfinder_cost or defense_mob_api.pathfinder.default_cost_method[prototype.movement],
		})
	end

	--Register for swarming
	if prototype.swarm
	then
		prototype.swarm = defense_mob_api.Swarm()
	end


	minetest.register_entity(name, prototype)
end
